Skip to content

Relax soc-targets along with the other SoC constraints - #2390

Merged
Flix6x merged 5 commits into
mainfrom
feat/relax-soc-targets
Aug 7, 2026
Merged

Relax soc-targets along with the other SoC constraints#2390
Flix6x merged 5 commits into
mainfrom
feat/relax-soc-targets

Conversation

@Flix6x

@Flix6x Flix6x commented Aug 4, 2026

Copy link
Copy Markdown
Member

Description

  • Relax soc-targets as a two-sided stock commitment instead of always enforcing them as hard equals constraints
  • Price the two directions with the existing breach prices: falling short of a target is a shortage (priced like a soc-minima breach), overshooting is a surplus (priced like a soc-maxima breach)
  • Report missed targets among the unresolved constraints, as the absolute deviation in either direction
  • Update the tests that relied on targets being hard, so they opt out of relaxation explicitly
  • Docs + changelog

Context

soc-targets was the last SoC constraint that constraint relaxation did not cover. An unreachable target made the whole scheduling problem infeasible, so one bad target cost the user their entire schedule — the outcome relax-constraints exists to prevent, and the same failure mode #2252 addresses by retiring the fallback scheduler.

Reusing soc-minima-breach-price and soc-maxima-breach-price means targets need no new flex-context field and follow relax-soc-constraints / relax-constraints automatically. Targets are relaxed only when both prices are available, which is the case whenever SoC relaxation is on, since the two defaults are filled in as a pair.

This is a piece of the wider physics-vs-preference discussion (a target is a preference, not a physical bound); by that reading it should never have been the one unconditionally hard constraint in the flex-model.

Impact

Breaking behaviour change. Under the default relax-constraints: True, an unreachable soc-targets entry now yields a schedule that gets as close as the hard constraints allow and pays the breach, instead of failing the job with "The input data yields an infeasible problem.".

To keep targets hard, set relax-constraints: False — and note that relax-soc-constraints: False alone does not achieve this, because relax-constraints defaults to True and still fills in the default SoC breach prices (see "Discovered along the way" below).

Three existing tests encoded the old behaviour and were updated to opt out of relaxation explicitly, rather than being weakened:

  • test_infeasible_problem_error — now runs with relax-constraints: False, and gains a sibling test_unreachable_soc_target_is_relaxed_by_default asserting the new path
  • test_numerical_errors — is about numerical error in the hard equals constraint, so it opts out
  • test_add_storage_constraint_from_sensor — inspects the equals column directly, so it opts out
  • test_get_job_status_failed_infeasible_schedule_includes_exc_info — used an unreachable target as its infeasibility source, so it opts out
  • test_off_tick_soc_target_extends_schedule_end_to_next_tick — needs both flags off; see below

Discovered along the way (not fixed here)

relax-soc-constraints: False does not actually keep SoC constraints hard while relax-constraints is True. In flexmeasures/data/schemas/scheduling/__init__.py, the guard reads

if (
    data["relax_soc_constraints"]
    or data["relax_constraints"]
    and not data.get("soc_minima_breach_price")
    and not data.get("soc_maxima_breach_price")
):

which parses as A or (B and C and D), so the not ... already set conditions only guard the relax_constraints branch. Two consequences, both pre-existing:

  1. relax-soc-constraints: False + default relax-constraints still fills in the default SoC breach prices.
  2. relax-soc-constraints: True overwrites explicitly-provided breach prices with the defaults, since set_default_breach_prices assigns unconditionally.

The same shape appears in the capacity and site-capacity blocks. This PR does not change it — fixing it would alter behaviour for soc-minima/soc-maxima and the capacity constraints too, which deserves its own PR. It is called out because this PR makes consequence 1 more visible: targets were previously the one constraint that stayed hard regardless.

How to test

pytest \
  flexmeasures/data/models/planning/tests/test_solver.py::test_unreachable_soc_target_is_relaxed_by_default \
  flexmeasures/data/models/planning/tests/test_solver.py::test_infeasible_problem_error \
  flexmeasures/data/models/planning/tests/test_storage.py::test_unresolved_targets_soc_targets \
  flexmeasures/data/models/planning/tests/ \
  flexmeasures/api/v3_0/tests/test_jobs_api.py \
  -v

Related Items


Sign-off

  • I agree to contribute to the project under Apache 2 License.
  • To the best of my knowledge, the proposed patch is not based on code under GPL or other license that is incompatible with FlexMeasures
  • Added changelog item in documentation/changelog.rst

🤖 Generated with Claude Code

https://claude.ai/code/session_014qPe5bv41RDTuBVtpdxjWH

An unreachable soc-target made the whole scheduling problem infeasible, so a
single bad target cost the user their entire schedule. Targets were the last
SoC constraint that constraint relaxation did not cover.

A target is a two-sided constraint, so relax it as a stock commitment priced in
both directions: falling short is a shortage, priced like a soc-minima breach,
and overshooting is a surplus, priced like a soc-maxima breach. Reusing the two
existing breach prices means targets need no new flex-context field and follow
relax-soc-constraints/relax-constraints automatically. Targets are relaxed only
when both prices are available, which is the case whenever SoC relaxation is on,
since the defaults are filled in as a pair.

Missed targets are now reported among the unresolved constraints, as the
absolute deviation in either direction. Since a met target leaves no headroom,
targets never produce a "resolved" entry.

Tests that relied on targets being hard now opt out of relaxation explicitly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qPe5bv41RDTuBVtpdxjWH
Signed-off-by: F.N. Claessen <claessen@seita.nl>
@read-the-docs-community

read-the-docs-community Bot commented Aug 4, 2026

Copy link
Copy Markdown

Signed-off-by: F.N. Claessen <claessen@seita.nl>

# Conflicts:
#	documentation/api/introduction.rst
#	documentation/changelog.rst
@Flix6x Flix6x added this to the 1.0.0 milestone Aug 7, 2026
Flix6x and others added 3 commits August 7, 2026 17:41
The soc-targets description still promised that an infeasible target prevents a
schedule from being computed, which this PR makes untrue, and the two SoC
breach-price descriptions did not mention that they now price targets as well.
These strings are what users read in the flex-model docs table and in OpenAPI,
so the generated specs move with them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Signed-off-by: F.N. Claessen <claessen@seita.nl>

# Conflicts:
#	documentation/api/introduction.rst
test_multi_device_validation_survives_stockless_device builds its invalid
constraint set from an unreachable soc-target, which this PR makes soft, so
validation had nothing left to report. Opt that test out of SoC relaxation, so it
keeps testing what its name says: that a stock-less device does not disable
validation for the devices after it.

Also correct the comment in the off-tick test. It claimed both relax flags were
needed, which stopped being true once #2252 made them tri-state; an explicit
relax-soc-constraints now stands on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: F.N. Claessen <claessen@seita.nl>
@Flix6x
Flix6x merged commit 1a14229 into main Aug 7, 2026
13 checks passed
@Flix6x
Flix6x deleted the feat/relax-soc-targets branch August 7, 2026 16:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant